<?php
/**
 * @link https://www.len168.com
 * @copyright Copyright (c) 2020/9/21 len168.com
 * @author toshcn <toshcn@foxmail.com>
 */

namespace common\actions;

use Yii;
use yii\helpers\Url;
use yii\web\Response;

/**
 * Class CaptchaAction 验证码
 * @package common\actions\CaptchaAction
 */
class CaptchaAction extends \yii\captcha\CaptchaAction
{
    /**
     * @var integer 干扰字符数量
     */
    public $disturbCharCount = 4;
    /**
     * @var integer 干扰字符循环次数
     */
    public $disturbCharLoop= 1;
    /**
     * @var int 使用Gd库生成的干扰字符字体放大倍数
     */
    public $noiseGdScale = 1;
    /**
     * @var int 使用Imagick库生成的干扰字符字体放大倍数
     */
    public $noiseImagickScale = 1;
    /**
     * @var bool 是否画干扰字符
     */
    public $isWriteNoise = true;
    /**
     * @var bool 是否画干扰线
     */
    public $isWriteCurve = true;
    /**
     * 验证码字体最小值
     * @var int
     */
    public $minFontSize = 30;
    /**
     * 验证码字体最大值
     * @var int
     */
    public $maxFontSize = 32;
    /**
     * 杂点字体最小值
     * @var int
     */
    public $noiseMinFontSize = 12;
    /**
     * 杂点字体最大值
     * @var int
     */
    public $noiseMaxFontSize = 16;
    /**
     * 验证码颜色随机
     * @var bool
     */
    public $foreColorIsRandom = false;

    /**
     * 验证码有效时间秒
     * @var bool
     */
    public $captchaCacheDuration = 600;

    /**
     * 字符间隔
     * @var int
     */
    public $offset = -2;

    /**
     * Runs the action.
     * @throws \yii\base\InvalidConfigException
     */
    public function run()
    {
        // 刷新AJAX方式返回
        if (Yii::$app->request->getQueryParam(self::REFRESH_GET_VAR) !== null) {
            // AJAX request for regenerating code
            $code = $this->getVerifyCode(true);
            Yii::$app->response->format = Response::FORMAT_JSON;
            return [
                'hash1' => $this->generateValidationHash($code),
                'hash2' => $this->generateValidationHash(strtolower($code)),
                // we add a random 'v' parameter so that FireFox can refresh the image
                // when src attribute of image tag is changed
                'url' => Url::to([$this->id, 'v' => uniqid('', true)]),
            ];
        }

        // 二进制数据
        $this->setHttpHeaders();
        Yii::$app->response->format = Response::FORMAT_RAW;

        // 刷新页面即更新验证码
        return $this->renderImage($this->getVerifyCode(false));
    }

    /**
     * 生成验证码 去掉session
     * Gets the verification code.
     * @param bool $regenerate whether the verification code should be regenerated.
     * @return string the verification code.
     */
    public function getVerifyCode($regenerate = false)
    {
        if ($this->fixedVerifyCode !== null) {
            return $this->fixedVerifyCode;
        }
        //保存到缓存
        $ip = md5(Yii::$app->getRequest()->getUserIP());
        $code = $this->generateVerifyCode();
        $key = Yii::$app->util->cacheKey('IMG_VERIFY_CODE_' . strtolower($code) . $ip);
        Yii::$app->getCache()->set($key, $code, $this->captchaCacheDuration);
        return $code;
    }


    /**
     * 验证
     * Validates the input to see if it matches the generated code.
     * @param string $input user input
     * @param bool $caseSensitive whether the comparison should be case-sensitive
     * @return bool whether the input is valid
     */
    public function validate($input, $caseSensitive)
    {
        $ip = md5(Yii::$app->getRequest()->getUserIP());
        $key = Yii::$app->util->cacheKey('IMG_VERIFY_CODE_' . strtolower($input) . $ip);
        $code = Yii::$app->getCache()->get($key);
        if ($code === false) return false;
        $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0;

        // 验证删除
        Yii::$app->getCache()->delete($key);
        return $valid;
    }


    /**
     * 使用GD库生成验证码
     * Renders the CAPTCHA image based on the code using GD library.
     * @param string $code the verification code
     * @return string image contents in PNG format.
     */
    protected function renderImageByGD($code)
    {
        $image = imagecreatetruecolor($this->width, $this->height);

        $backColor = imagecolorallocate(
            $image,
            (int)($this->backColor % 0x1000000 / 0x10000),
            (int)($this->backColor % 0x10000 / 0x100),
            $this->backColor % 0x100
        );

        imagefilledrectangle($image, 0, 0, $this->width - 1, $this->height - 1, $backColor);
        imagecolordeallocate($image, $backColor);
        if ($this->transparent) {
            imagecolortransparent($image, $backColor);
        }
        if ($this->foreColorIsRandom === true) {
            $foreColor = imagecolorallocate(
                $image,
                mt_rand(20, 205),
                mt_rand(20, 205),
                mt_rand(20, 205)
            );
        } else {
            $foreColor = imagecolorallocate(
                $image,
                (int)($this->foreColor % 0x1000000 / 0x10000),
                (int)($this->foreColor % 0x10000 / 0x100),
                (int)$this->foreColor % 0x100
            );
        }
        $length = strlen($code);
        $box = imagettfbbox(30, 0, $this->fontFile, $code);
        $w = $box[4] - $box[0] + $this->offset * ($length - 1);
        $h = $box[1] - $box[5];
        $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h);
        $x = 5;
        $y = round($this->height * 27 / 40);
        // 画干扰点
        $this->_writeNoise($image);
        // 干扰线
        $this->_writeCurve($image);
        for ($i = 0; $i < $length; ++$i) {
            $fontSize = (int)(mt_rand($this->minFontSize, $this->maxFontSize) * $scale);
            $angle = mt_rand(-10, 5);
            $letter = $code[$i];
            $box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter);
            $x = $box[2] + $this->offset;
        }
        // 干扰线
        $this->_writeCurve($image);
        imagecolordeallocate($image, $foreColor);
        ob_start();
        imagepng($image);
        imagedestroy($image);
        return ob_get_clean();
    }

    /**
     * 使用ImageMagick图形库
     * Renders the CAPTCHA image based on the code using ImageMagick library.
     * @param string $code the verification code
     * @return string image contents in PNG format.
     * @throws \ImagickException
     */
    protected function renderImageByImagick($code)
    {
        $backColor = $this->transparent ? new \ImagickPixel('transparent') : new \ImagickPixel('#' . str_pad(dechex($this->backColor), 6, 0, STR_PAD_LEFT));
        if ($this->foreColorIsRandom === true) {
            $r = mt_rand(20, 205);
            $r = $r < 16 ? '0' . dechex($r) : dechex($r);
            $g = mt_rand(20, 205);
            $g = $g < 16 ? '0' . dechex($g) : dechex($g);
            $b = mt_rand(20, 205);
            $b = $b < 16 ? '0' . dechex($b) : dechex($b);
            $foreColor = new \ImagickPixel('#' . str_pad("{$r}{$g}{$b}", 6, 0, STR_PAD_LEFT));
        } else {
            $foreColor = new \ImagickPixel('#' . str_pad(dechex($this->foreColor), 6, 0, STR_PAD_LEFT));
        }
        $image = new \Imagick();
        $image->newImage($this->width, $this->height, $backColor);
        $draw = new \ImagickDraw();
        $draw->setFont($this->fontFile);
        $draw->setFontSize(30);
        $fontMetrics = $image->queryFontMetrics($draw, $code);
        $length = strlen($code);
        $w = (int)$fontMetrics['textWidth'] - 8 + $this->offset * ($length - 1);
        $h = (int)$fontMetrics['textHeight'] - 8;
        $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h);
        $x = 5;
        $y = round($this->height * 27 / 40);
        // 添加杂点
        $this->_writeNoiseImagick($image);
        // 干扰线
        $this->_writeCurveImagick($image);
        for ($i = 0; $i < $length; ++$i) {
            $draw = new \ImagickDraw();
            $draw->setFont($this->fontFile);
            $draw->setFontSize((int)(mt_rand($this->minFontSize, $this->maxFontSize) * $scale));
            $draw->setFillColor($foreColor);
            $image->annotateImage($draw, $x, $y, mt_rand(-10, 10), $code[$i]);
            $fontMetrics = $image->queryFontMetrics($draw, $code[$i]);
            $x += (int)$fontMetrics['textWidth'] + $this->offset;
        }
        // 干扰线
        $this->_writeCurveImagick($image);
        $image->setImageFormat('png');
        return $image->getImageBlob();
    }

    /**
     * 往图片上写不同颜色的字母或数字
     * @param $image object gd image
     * @param int $scale 干扰字符字体放大倍数
     */
    private function _writeNoise(&$image, $scale = 0)
    {
        if ($this->isWriteNoise === true && !empty($image)) {
            $scale = $scale ? $scale : $this->noiseGdScale;
            $codeSet = '0123456789abcdefhijklmnopqrstuvwxyz';
            $strLength = strlen($codeSet) - 1;
            for ($i = 0; $i < $this->disturbCharCount; $i++) {
                //杂点颜色
                $noiseColor = imagecolorallocate($image, mt_rand(10, 250), mt_rand(10, 250), mt_rand(10, 250));

                for ($j = 0; $j < $this->disturbCharLoop; $j++) {
                    // 绘杂点
                    imagettftext($image, (int)(mt_rand($this->noiseMinFontSize, $this->noiseMaxFontSize) * $scale), mt_rand(-10, 200), mt_rand(-10, $this->width), mt_rand(-10, $this->height), $noiseColor, $this->fontFile, $codeSet[mt_rand(0, $strLength)]);
                }
            }
        }
    }

    /**
     * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
     *
     *      高中的数学公式咋都忘了涅，写出来
     *      正弦型函数解析式：y=Asin(ωx+φ)+b
     *      各常数值对函数图像的影响：
     *        A：决定峰值（即纵向拉伸压缩的倍数）
     *        b：表示波形在Y轴的位置关系或纵向移动距离（上加下减）
     *        φ：决定波形与X轴位置关系或横向移动距离（左加右减）
     *        ω：决定周期（最小正周期T=2π/∣ω∣）
     * @param &$image object image
     */
    private function _writeCurve(&$image)
    {
        if ($this->isWriteCurve === true) {
            $py = 0;
            // 曲线前部分
            $A = mt_rand(1, $this->height / 2);                  // 振幅
            $b = mt_rand(-$this->height / 4, $this->height / 4);   // Y轴方向偏移量
            $f = mt_rand(-$this->height / 4, $this->height / 4);   // X轴方向偏移量
            $T = mt_rand($this->height, $this->width * 2);  // 周期
            $w = (2 * M_PI) / $T;
            $px1 = 0;  // 曲线横坐标起始位置
            $px2 = mt_rand($this->width / 2, $this->width * 0.8);  // 曲线横坐标结束位置
            for ($px = $px1; $px <= $px2; $px = $px + 1) {
                if ($w != 0) {
                    $py = $A * sin($w * $px + $f) + $b + $this->height / 2;  // y = Asin(ωx+φ) + b
                    $i = 3;
                    while ($i > 0) {
                        // 杂点颜色
                        $noiseColor = imagecolorallocate($image, mt_rand(1, 255), mt_rand(1, 255), mt_rand(1, 255));
                        // 这里(while)循环画像素点比imagettftext和imagestring用字体大小一次画出（不用这while循环）性能要好很多
                        imagesetpixel($image, $px + $i, $py + $i, $noiseColor);
                        $i--;
                    }
                }
            }
            // 曲线后部分
            $A = mt_rand(1, $this->height / 2);                  // 振幅
            $f = mt_rand(-$this->height / 4, $this->height / 4);   // X轴方向偏移量
            $T = mt_rand($this->height, $this->width * 2);  // 周期
            $w = (2 * M_PI) / $T;
            $b = $py - $A * sin($w * $px + $f) - $this->height / 2;
            $px1 = $px2;
            $px2 = $this->width;
            for ($px = $px1; $px <= $px2; $px = $px + 1) {
                if ($w != 0) {
                    $py = $A * sin($w * $px + $f) + $b + $this->height / 2;  // y = Asin(ωx+φ) + b
                    $i = 3;
                    while ($i > 0) {
                        //杂点颜色
                        $noiseColor = imagecolorallocate($image, mt_rand(1, 255), mt_rand(1, 255), mt_rand(1, 255));
                        imagesetpixel($image, $px + $i, $py + $i, $noiseColor);
                        $i--;
                    }
                }
            }
        }
    }

    /**
     * 往图片上写不同颜色的字母或数字
     * @param &$image object imagick image
     * @param int $scale 干扰字符字体放大倍数
     */
    private function _writeNoiseImagick(&$image, $scale = 0)
    {
        if ($this->isWriteNoise === true) {
            $scale = $scale ? $scale : $this->noiseImagickScale;
            $codeSet = '012345678abcdefhijkmnopqrstuvwxyz';
            $strLength = strlen($codeSet) - 1;
            for ($i = 0; $i < $this->disturbCharCount; $i++) {
                //杂点颜色
                $r = mt_rand(1, 155);
                $r = $r < 16 ? '0' . dechex($r) : dechex($r);
                $g = mt_rand(1, 155);
                $g = $g < 16 ? '0' . dechex($g) : dechex($g);
                $b = mt_rand(1, 155);
                $b = $b < 16 ? '0' . dechex($b) : dechex($b);
                $noiseColor = new \ImagickPixel('#' . str_pad("{$r}{$g}{$b}", 6, 0, STR_PAD_LEFT));
                for ($j = 0; $j < $this->disturbCharLoop; $j++) {
                    // 绘杂点
                    $index = mt_rand(0, $strLength);
                    $draw = new \ImagickDraw();
                    $draw->setFont($this->fontFile);
                    $draw->setFontSize((int)(mt_rand($this->noiseMinFontSize, $this->noiseMaxFontSize) * $scale));
                    $draw->setFillColor($noiseColor);
                    $image->annotateImage($draw, mt_rand(-10, $this->width), mt_rand(-10, $this->height), mt_rand(-10, mt_rand(10, 200)), $codeSet[$index]);
                }
            }
        }
    }

    /**
     * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
     *
     *      高中的数学公式咋都忘了涅，写出来
     *      正弦型函数解析式：y=Asin(ωx+φ)+b
     *      各常数值对函数图像的影响：
     *        A：决定峰值（即纵向拉伸压缩的倍数）
     *        b：表示波形在Y轴的位置关系或纵向移动距离（上加下减）
     *        φ：决定波形与X轴位置关系或横向移动距离（左加右减）
     *        ω：决定周期（最小正周期T=2π/∣ω∣）
     * @param &$image [Imagick image]
     */
    private function _writeCurveImagick(&$image)
    {
        if ($this->isWriteCurve === true) {
            $py = 0;
            // 曲线前部分
            $A = mt_rand(1, $this->height / 2);                  // 振幅
            $bx = mt_rand(-$this->height / 2, $this->height / 2);   // Y轴方向偏移量
            $f = mt_rand(-$this->height / 4, $this->height / 4);   // X轴方向偏移量
            $T = mt_rand($this->height, $this->width * 2);  // 周期
            $w = floatval(2 * M_PI / $T);
            $px1 = 0;  // 曲线横坐标起始位置
            $px2 = mt_rand($this->width / 2, $this->width * 0.8);  // 曲线横坐标结束位置
            $draw = new \ImagickDraw();
            for ($px = $px1; $px <= $px2; $px = $px + 1) {
                if ($w != 0) {
                    $py = $A * sin($w * $px + $f) + $bx + $this->height / 2;  // y = Asin(ωx+φ) + b
                    $i = 3;
                    while ($i > 0) {
                        // 杂点颜色
                        $r = mt_rand(1, 255);
                        $r = $r < 16 ? '0' . dechex($r) : dechex($r);
                        $g = mt_rand(1, 255);
                        $g = $g < 16 ? '0' . dechex($g) : dechex($g);
                        $b = mt_rand(1, 255);
                        $b = $b < 16 ? '0' . dechex($b) : dechex($b);
                        $noiseColor = new \ImagickPixel('#' . str_pad("{$r}{$g}{$b}", 6, 0, STR_PAD_LEFT));
                        // 绘制
                        $draw->setFillColor($noiseColor);
                        $draw->point($px + $i, $py + $i);
                        $i--;
                    }
                }
            }
            // 曲线后部分
            $A = mt_rand(1, $this->height / 2);                  // 振幅
            $f = mt_rand(-$this->height / 4, $this->height / 4);   // X轴方向偏移量
            $T = mt_rand($this->height, $this->width * 2);  // 周期
            $w = (2 * M_PI) / $T;
            $bx = $py - $A * sin($w * $px + $f) - $this->height / 2;
            $px1 = $px2;
            $px2 = $this->width;
            for ($px = $px1; $px <= $px2; $px = $px + 1) {
                if ($w != 0) {
                    $py = $A * sin($w * $px + $f) + $bx + $this->height / 2;  // y = Asin(ωx+φ) + b
                    $i = 3;
                    while ($i > 0) {
                        // 杂点颜色
                        $r = mt_rand(1, 255);
                        $r = $r < 16 ? '0' . dechex($r) : dechex($r);
                        $g = mt_rand(1, 255);
                        $g = $g < 16 ? '0' . dechex($g) : dechex($g);
                        $b = mt_rand(1, 255);
                        $b = $b < 16 ? '0' . dechex($b) : dechex($b);
                        $noiseColor = new \ImagickPixel('#' . str_pad("{$r}{$g}{$b}", 6, 0, STR_PAD_LEFT));
                        // 绘制
                        $draw->setFillColor($noiseColor);
                        $draw->point($px + $i, $py + $i);
                        $i--;
                    }
                }
            }
            $image->drawImage($draw);
        }
    }
}
